type P = [number, number] | null

interface PinSlot {
  id: number
  pin: P
  slot: P
  children: PinSlot[]
}

interface TreeData {
  id: number
  pid: number
  label: string
  color?: string
  children?: TreeData[]
}

export default class LineFlow {
  canvas: HTMLCanvasElement | null = null
  ctx: CanvasRenderingContext2D | null = null
  linkCanvas: HTMLCanvasElement | null = null
  linkCtx: CanvasRenderingContext2D | null = null
  treeOrgElem: HTMLElement | null
  rootNodeElem: HTMLElement | null
  readonly treeData: TreeData
  pinSlot: PinSlot | null = null
  duration: number = 1500
  t: number = 6
  width: number = 0
  height: number = 0
  rq: number = 0
  padding: number = 40

  constructor(treeData: TreeData) {
    this.treeData = treeData
    this.treeOrgElem = document.querySelector('.config-tree-org .tree-org')
    this.rootNodeElem = document.querySelector('.config-tree-org .tree-org > .tree-org-node')
    this.init()
  }

  init() {
    this.createCanvas()
    this.getPinSlot()
    this.draw()
  }

  createCanvas() {
    if (document.getElementById('line-flow-canvas'))
      return

    const canvas = this.canvas = document.createElement('canvas')
    const linkCanvas = this.linkCanvas = document.createElement('canvas')
    const width = this.treeOrgElem?.clientWidth || 0
    const height = this.treeOrgElem?.clientHeight || 0

    canvas.width = width
    canvas.height = height
    linkCanvas.width = width
    linkCanvas.height = height
    this.width = width
    this.height = height

    this.ctx = canvas.getContext('2d')
    this.linkCanvas = linkCanvas
    this.linkCtx = linkCanvas.getContext('2d')
    canvas.setAttribute('id', 'line-flow-canvas')
    canvas.setAttribute('style', 'position: absolute; top: 0; left: 0;')

    this.treeOrgElem?.parentNode?.insertBefore(canvas, this.treeOrgElem)
  }

  getPinSlot() {
    const { treeData } = this
    const getPin = (id: number): P => {
      const elem = document.getElementById(`node-content-${id}`)
      if (elem) {
        const w = elem.clientWidth
        const l = elem.offsetLeft
        const t = elem.offsetTop
        return [l + w / 2, t]
      }
      return null
    }
    const getSlot = (id: number): P => {
      const elem = document.getElementById(`node-content-${id}`)
      if (elem) {
        const w = elem.clientWidth
        const h = elem.clientHeight
        const l = elem.offsetLeft
        const t = elem.offsetTop
        return [l + w / 2, t + h]
      }
      return null
    }
    const recursive = (array: TreeData[]): PinSlot[] => {
      const ret: PinSlot[] = []
      for (const item of array) {
        ret.push({
          id: item.id,
          pin: getPin(item.id),
          slot: getSlot(item.id),
          children: recursive(item.children || []),
        })
      }
      return ret
    }

    this.pinSlot = {
      id: treeData.id,
      pin: getPin(treeData.id),
      slot: getSlot(treeData.id),
      children: recursive(treeData.children || []),
    }
  }

  link(s: P, e: P) {
    const { linkCtx, padding } = this
    if (!linkCtx || !s || !e)
      return

    linkCtx.beginPath()
    linkCtx.moveTo(...s)

    // 伸出两个引脚线
    linkCtx.lineTo(
      s[0],
      s[1] + padding,
    )
    linkCtx.lineTo(
      e[0],
      e[1] - padding,
    )

    linkCtx.lineTo(...e)
    linkCtx.strokeStyle = '#dfdfdf'
    linkCtx.lineWidth = 6
    linkCtx.stroke()
  }

  path(s: P, e: P) {
    const { ctx } = this
    if (!ctx || !s || !e)
      return

    ctx.beginPath()
    ctx.moveTo(...s)
    ctx.lineTo(...e)
    ctx.strokeStyle = 'red'
    ctx.lineWidth = 3
    ctx.stroke()
  }

  recursiveLink(ps: PinSlot) {
    for (const child of ps.children) {
      this.link(ps.slot, child.pin)
      this.recursiveLink(child)
    }
  }

  drawLink() {
    const { pinSlot, ctx, linkCtx } = this
    if (!pinSlot || !ctx || !linkCtx)
      return
    this.recursiveLink(pinSlot)
    this.linkCanvas && ctx.drawImage(this.linkCanvas, 0, 0)
  }

  generatePrev(s: P, e: P, progress: number) {
    if (!s || !e)
      return null
    const TX = e[0] - s[0]
    const TY = e[1] - s[1]
    const K = TY / TX

    // s,e是同一个点
    if (Number.isNaN(K))
      return null

    let prev = [
      K < 0
        ? Math.max(s[0] + TX * progress, e[0])
        : Math.min(s[0] + TX * progress, e[0]),
      Math.min(K * TX * progress + s[1], e[1]),
    ] as [number, number]

    // 当直线是竖线时，斜率为无穷大
    if (K === Infinity || K === -Infinity) {
      prev = [
        s[0],
        s[1] + TY * progress,
      ]
    }

    // 当直线是横线时，斜率为0
    if (Math.abs(K) === 0) {
      prev = [
        TX > 0
          ? Math.min(s[0] + TX * progress, e[0])
          : Math.max(s[0] + TX * progress, e[0]),
        s[1],
      ]
    }

    return prev
  }

  generateNext(s: P, e: P, prev: P) {
    if (!prev || !e || !s)
      return null
    const L = this.t
    const TX = e[0] - s[0]
    const TY = e[1] - s[1]
    const K = TY / TX

    if (Number.isNaN(K))
      return null

    let next = [
      K < 0
        ? Math.max(prev[0] - L, e[0])
        : Math.min(prev[0] + L, e[0]),
      Math.min(K * ((K < 0 ? -1 : 1) * L) + prev[1], e[1]),
    ] as [number, number]

    // 当直线是竖线时，斜率为无穷大
    if (K === Infinity || K === -Infinity) {
      next = [prev[0], prev[1] + L]
    }

    // 当直线是横线时，斜率为0
    if (Math.abs(K) === 0) {
      next = [
        TX > 0
          ? prev[0] + L
          : prev[0] - L,
        prev[1],
      ]
    }
    return next
  }

  recursivePath(ps: PinSlot, progress: number) {
    const padding = this.padding
    for (const child of ps.children) {
      const s = ps.slot
      const e = child.pin
      if (!s || !e)
        continue

      const s1 = [
        s[0],
        s[1] + padding,
      ] as P
      const e1 = [
        e[0],
        e[1] - padding,
      ] as P

      const prev1 = this.generatePrev(s, s1, progress)
      const next1 = this.generateNext(s, s1, prev1)

      const prev2 = this.generatePrev(s1, e1, progress)
      const next2 = this.generateNext(s1, e1, prev2)

      const prev3 = this.generatePrev(e1, e, progress)
      const next3 = this.generateNext(e1, e, prev3)

      this.path(prev1, next1)
      this.path(prev2, next2)
      this.path(prev3, next3)

      this.recursivePath(child, progress)
    }
  }

  drawPath() {
    const { pinSlot, ctx, linkCtx, linkCanvas } = this
    if (!pinSlot || !ctx || !linkCtx || !linkCanvas)
      return

    let time = 0
    const step = (timestamp: number) => {
      !time && (time = timestamp)
      const t = timestamp - time
      const progress = Math.min(t / this.duration, 1)

      ctx.clearRect(0, 0, this.width, this.height)
      ctx.drawImage(this.linkCanvas!, 0, 0)
      this.recursivePath(pinSlot, progress)
      if (progress >= 0.8) {
        time = 0
      }
      this.rq = requestAnimationFrame(step)
    }
    this.rq = requestAnimationFrame(step)
  }

  clean() {
    this.rq && cancelAnimationFrame(this.rq)
  }

  draw() {
    this.ctx?.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height)
    this.linkCtx?.clearRect(0, 0, this.linkCtx.canvas.width, this.linkCtx.canvas.height)

    this.clean()
    this.drawLink()
    this.drawPath()
  }

  update() {
    this.clean()
    const { canvas, linkCanvas } = this
    if (!canvas || !linkCanvas)
      return
    const width = this.treeOrgElem?.clientWidth || 0
    const height = this.treeOrgElem?.clientHeight || 0

    canvas.width = width
    canvas.height = height
    linkCanvas.width = width
    linkCanvas.height = height
    this.width = width
    this.height = height

    this.getPinSlot()
    this.draw()
  }
}
